#include "cfg.h"
#include "INIReader.h"
#include "common/file.h"
#include "common/log.h"


namespace evm { namespace config {

constexpr const char* DEFAULT_GROUP_NAME = "default";
const Group DEFAULT_GROUP{DEFAULT_GROUP_NAME};

ConfigOpts::ConfigOpts(string program, string default_config_file) {
    this->program = program;
    this->default_config_file = default_config_file;

    (*this)[string(DEFAULT_GROUP_NAME)] = Json::object();
    (*this)["_meta"] = Json::object();
}

void ConfigOpts::operator()(int argc, const char** argv) {
    if (program.empty())
        program = argv[0];
    if (default_config_file.empty())
        default_config_file = program + ".conf";

    register_cli_opt("config_file", default_config_file, "Path to config file to use.");

    cli.set_program_name(program);
    if (argc)
        cli.parse_check(argc, argv);
    else {
        const char* arg = program.data();
        cli.parse_check(1, &arg);
    }

    string config_file = cli.get<string>("config_file");
    log_info("config_file: {}", config_file);
    string err_msg = "invalid config file " + config_file;

    if (str::endswith(config_file, ".json")) {  // if json format, do not support option group
        Json c = Json::parse(File::read_all(config_file));
        if (!c.is_object()) {
            throw_with_log(err_msg + ": json config should be an object");
        }

        log_info(str::replace(str::replace(c.dump(), "{", "{{"), "}", "}}"));  // escape { and }

        for (auto ci = c.begin(); ci != c.end(); ++ci) {
            const auto& opt_name = ci.key();

            if (!group().count(opt_name))
                throw_with_log(err_msg + ": invalid option " + opt_name);

            if (ci.value().type() != group()[opt_name].type())
                throw_with_log(err_msg + ": invalid option value " + opt_name);

            group()[opt_name] = ci.value();
            (*this)[opt_name] = ci.value();
        }

        const auto& meta = this->operator[]("_meta")[DEFAULT_GROUP()];
        for (auto it = meta.cbegin(); it != meta.cend(); ++it) {
            if (!it.value()["cli"] && it.value()["required"] && !c.count(it.key()))
                throw_with_log(err_msg + ": lack option " + it.key());
        }
    }
    else {  // if ini format
        INIReader reader(config_file);
        if (reader.ParseError() < 0)
            throw_with_log(err_msg);

        for (const auto& group_name: reader.GetSections()) {
            if (!count(group_name))
                throw_with_log(err_msg + ": invalid option group " + group_name);

            for (const auto& opt_name: reader.GetFields(group_name)) {
                if (!group(group_name).count(opt_name))
                    throw_with_log(err_msg + ": invalid option " + group_name + "." + opt_name);

                auto& opt_value = group(group_name)[opt_name];
                if (opt_value.is_string())
                    opt_value = reader.Get(group_name, opt_name, "");
                else if (opt_value.is_boolean())
                    opt_value = reader.GetBoolean(group_name, opt_name, false);
                else if (opt_value.is_number_integer())
                    opt_value = reader.GetInteger(group_name, opt_name, 0);
                else if (opt_value.is_number_float())
                    opt_value = reader.GetReal(group_name, opt_name, 0.);
                else
                    throw_with_log(err_msg + ": invalid option value "
                                   + group_name + "." + opt_name + "=" + reader.Get(group_name, opt_name, ""));

                if (group_name == DEFAULT_GROUP())
                    (*this)[opt_name] = opt_value;
            }
        }
    }

    is_parsed = true;
}

void ConfigOpts::log() const {
    const auto& meta = this->operator[]("_meta");
    for (auto gi = meta.cbegin(); gi != meta.cend(); ++gi) {
        log_info("option group: {}", gi.key());
        const auto& g = this->operator[](gi.key());
        for (auto oi = gi.value().cbegin(); oi != gi.value().cend(); ++oi) {
            log_info("option [ name: {}, value: {}, default: {}, required: {}, help: {} ]",
                     oi.key(), g[oi.key()].dump(),
                     oi.value()["default"].dump(), oi.value()["required"].dump(), oi.value()["help"].dump());
        }
    }
}

}}
